msg_tool\scripts\kirikiri\archive/
xp3.rs

1use super::xp3pack::*;
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::Result;
6use flate2::read::ZlibDecoder;
7use overf::wrapping;
8use std::io::{Read, Seek, SeekFrom, Take};
9use std::sync::{Arc, Mutex};
10use xp3::XP3Reader;
11use xp3::index::file::{IndexSegmentFlag, XP3FileIndex};
12
13pub use super::xp3pack::SegmenterConfig;
14
15pub fn parse_segmenter_config(str: &str) -> Result<SegmenterConfig> {
16    let parts: Vec<&str> = str.split(':').collect();
17    if parts.is_empty() {
18        return Ok(SegmenterConfig::default());
19    }
20    match parts[0].to_lowercase().as_str() {
21        "none" => Ok(SegmenterConfig::None),
22        "cdc" => {
23            if parts.len() != 4 {
24                return Err(anyhow::anyhow!(
25                    "Invalid FastCDC segmenter config. Expected format: fastcdc,min_size,avg_size,max_size"
26                ));
27            }
28            let min_size = parse_size::parse_size(parts[1])?;
29            let avg_size = parse_size::parse_size(parts[2])?;
30            let max_size = parse_size::parse_size(parts[3])?;
31            if min_size == 0 || avg_size == 0 || max_size == 0 {
32                return Err(anyhow::anyhow!(
33                    "Invalid FastCDC segmenter config. Sizes must be greater than 0."
34                ));
35            }
36            if !(min_size <= avg_size && avg_size <= max_size) {
37                return Err(anyhow::anyhow!(
38                    "Invalid FastCDC segmenter config. Expected min_size <= avg_size <= max_size."
39                ));
40            }
41            Ok(SegmenterConfig::FastCdc {
42                min_size: min_size as u32,
43                avg_size: avg_size as u32,
44                max_size: max_size as u32,
45            })
46        }
47        "fixed" => {
48            if parts.len() != 2 {
49                return Err(anyhow::anyhow!(
50                    "Invalid Fixed segmenter config. Expected format: fixed,size"
51                ));
52            }
53            let size = parse_size::parse_size(parts[1])?;
54            if size == 0 {
55                return Err(anyhow::anyhow!(
56                    "Invalid Fixed segmenter config. Size must be greater than 0."
57                ));
58            }
59            Ok(SegmenterConfig::Fixed(size as usize))
60        }
61        _ => Err(anyhow::anyhow!("Unknown segmenter type: {}", parts[0])),
62    }
63}
64
65#[derive(Debug)]
66/// Builder for Kirikiri XP3 Archive
67pub struct Xp3ArchiveBuilder {}
68
69impl Xp3ArchiveBuilder {
70    /// Create a new Kirikiri XP3 Archive Builder
71    pub fn new() -> Self {
72        Self {}
73    }
74}
75
76impl ScriptBuilder for Xp3ArchiveBuilder {
77    fn default_encoding(&self) -> Encoding {
78        Encoding::Utf8
79    }
80
81    fn default_archive_encoding(&self) -> Option<Encoding> {
82        Some(Encoding::Utf8)
83    }
84
85    fn build_script(
86        &self,
87        buf: Vec<u8>,
88        _filename: &str,
89        _encoding: Encoding,
90        _archive_encoding: Encoding,
91        config: &ExtraConfig,
92        _archive: Option<&Box<dyn Script>>,
93    ) -> Result<Box<dyn Script>> {
94        Ok(Box::new(Xp3Archive::new(MemReader::new(buf), config)?))
95    }
96
97    fn build_script_from_file(
98        &self,
99        filename: &str,
100        _encoding: Encoding,
101        _archive_encoding: Encoding,
102        config: &ExtraConfig,
103        _archive: Option<&Box<dyn Script>>,
104    ) -> Result<Box<dyn Script>> {
105        let file = std::fs::File::open(filename)?;
106        Ok(Box::new(Xp3Archive::new(file, config)?))
107    }
108
109    fn build_script_from_reader(
110        &self,
111        reader: Box<dyn ReadSeek>,
112        _filename: &str,
113        _encoding: Encoding,
114        _archive_encoding: Encoding,
115        config: &ExtraConfig,
116        _archive: Option<&Box<dyn Script>>,
117    ) -> Result<Box<dyn Script>> {
118        Ok(Box::new(Xp3Archive::new(reader, config)?))
119    }
120
121    fn extensions(&self) -> &'static [&'static str] {
122        &["xp3"]
123    }
124
125    fn script_type(&self) -> &'static ScriptType {
126        &ScriptType::KirikiriXp3
127    }
128
129    fn is_archive(&self) -> bool {
130        true
131    }
132
133    fn create_archive(
134        &self,
135        filename: &str,
136        files: &[&str],
137        _encoding: Encoding,
138        config: &ExtraConfig,
139    ) -> Result<Box<dyn Archive>> {
140        Ok(Box::new(Xp3ArchiveWriter::new(filename, files, config)?))
141    }
142}
143
144#[derive(Debug)]
145/// Kirikiri XP3 Archive
146pub struct Xp3Archive<T: Read + Seek + std::fmt::Debug> {
147    reader: Arc<Mutex<T>>,
148    entries: Vec<(String, XP3FileIndex)>,
149    decrypt_simple_crypt: bool,
150    decompress_mdf: bool,
151}
152
153impl<T: Read + Seek + std::fmt::Debug> Xp3Archive<T> {
154    /// Create a new Kirikiri XP3 Archive
155    pub fn new(reader: T, config: &ExtraConfig) -> Result<Self> {
156        let xp3_reader = XP3Reader::open_archive(reader)
157            .map_err(|e| anyhow::anyhow!("Failed to open XP3 archive: {:?}", e))?;
158        let entries = xp3_reader
159            .entries()
160            .filter_map(|(i, d)| {
161                // Skip garbage files
162                if i.find("$$$ This is a protected archive. $$$").is_some()
163                    || (i.to_lowercase().ends_with(".nene") && d.info().file_size() == 0)
164                {
165                    None
166                } else {
167                    Some((i.clone(), d.clone()))
168                }
169            })
170            .collect();
171        Ok(Self {
172            reader: Arc::new(Mutex::new(xp3_reader.close().1)),
173            entries,
174            decrypt_simple_crypt: config.xp3_simple_crypt,
175            decompress_mdf: config.xp3_mdf_decompress,
176        })
177    }
178}
179
180impl<T: Read + Seek + std::fmt::Debug + 'static> Script for Xp3Archive<T> {
181    fn default_output_script_type(&self) -> OutputScriptType {
182        OutputScriptType::Json
183    }
184
185    fn default_format_type(&self) -> FormatOptions {
186        FormatOptions::None
187    }
188
189    fn is_archive(&self) -> bool {
190        true
191    }
192
193    fn iter_archive_filename<'a>(
194        &'a self,
195    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
196        Ok(Box::new(
197            self.entries.iter().map(|entry| Ok(entry.0.clone())),
198        ))
199    }
200
201    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
202        let index = self
203            .entries
204            .iter()
205            .nth(index)
206            .ok_or(anyhow::anyhow!("Index out of bounds: {}", index))?
207            .1
208            .clone();
209        let mut entry = Entry::new(self.reader.clone(), index);
210        let mut header = [0u8; 16];
211        let header_len = entry.read(&mut header)?;
212        entry.rewind()?;
213        entry.script_type = detect_script_type(entry.index.info().name(), &header, header_len);
214        if self.decrypt_simple_crypt
215            && header_len >= 5
216            && header[0] == 0xFE
217            && header[1] == 0xFE
218            && header[3] == 0xFF
219            && header[4] == 0xFE
220        {
221            let crypt = header[2];
222            if crypt == 2 {
223                let index = entry.index.clone();
224                return Ok(Box::new(SimpleCryptZlib::new(entry, index)?));
225            }
226            if matches!(crypt, 0 | 1) {
227                let index = entry.index.clone();
228                return Ok(Box::new(SimpleCrypt::new(entry, index, crypt)?));
229            }
230        }
231        if self.decompress_mdf
232            && header_len >= 4
233            && &header[0..4] == b"mdf\0"
234            && entry.index.info().file_size() > 8
235        {
236            let index = entry.index.clone();
237            return Ok(Box::new(MdfEntry::new(entry, index)?));
238        }
239        Ok(Box::new(entry))
240    }
241}
242
243fn detect_script_type(filename: &str, buf: &[u8], buf_len: usize) -> Option<ScriptType> {
244    #[cfg(feature = "kirikiri-img")]
245    if buf_len >= 11 && libtlg_rs::is_valid_tlg(buf) {
246        return Some(ScriptType::KirikiriTlg);
247    }
248    if buf_len >= 8 && (buf.starts_with(b"TJS/ns0\0") || buf.starts_with(b"TJS/4s0\0")) {
249        return Some(ScriptType::KirikiriTjsNs0);
250    }
251    if buf_len >= 8 && buf.starts_with(b"TJS2100\0") {
252        return Some(ScriptType::KirikiriTjs2);
253    }
254    let extension = std::path::Path::new(filename)
255        .extension()
256        .and_then(|s| s.to_str())
257        .unwrap_or("")
258        .to_lowercase();
259    match extension.as_str() {
260        "ks" => Some(ScriptType::Kirikiri),
261        "scn" => Some(ScriptType::KirikiriScn),
262        #[cfg(feature = "emote-img")]
263        "dref" => Some(ScriptType::EmoteDref),
264        #[cfg(feature = "emote-img")]
265        "pimg" => Some(ScriptType::EmotePimg),
266        _ => None,
267    }
268}
269
270#[derive(Debug)]
271struct Entry<T: Read + Seek + std::fmt::Debug> {
272    reader: Arc<Mutex<T>>,
273    index: XP3FileIndex,
274    cache: Option<ZlibDecoder<Take<MutexWrapper<T>>>>,
275    pos: u64,
276    entries_pos: Vec<u64>,
277    script_type: Option<ScriptType>,
278}
279
280impl<T: Read + Seek + std::fmt::Debug> Entry<T> {
281    fn new(reader: Arc<Mutex<T>>, index: XP3FileIndex) -> Self {
282        let mut pos = 0;
283        let entries_pos = index
284            .segments()
285            .iter()
286            .map(|seg| {
287                let p = pos;
288                pos += seg.original_size();
289                p
290            })
291            .collect();
292        Self {
293            reader,
294            index,
295            cache: None,
296            pos: 0,
297            entries_pos,
298            script_type: None,
299        }
300    }
301}
302
303impl<T: Read + Seek + std::fmt::Debug> ArchiveContent for Entry<T> {
304    fn name(&self) -> &str {
305        &self.index.info().name()
306    }
307
308    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
309        Ok(Box::new(self))
310    }
311
312    fn script_type(&self) -> Option<&ScriptType> {
313        self.script_type.as_ref()
314    }
315}
316
317impl<T: Read + Seek + std::fmt::Debug> Read for Entry<T> {
318    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
319        if self.pos >= self.index.info().file_size() {
320            self.cache.take();
321            return Ok(0);
322        }
323        if let Some(cache) = self.cache.as_mut() {
324            let readed = cache.read(buf)?;
325            if readed > 0 {
326                self.pos += readed as u64;
327                return Ok(readed);
328            }
329            self.cache.take();
330        }
331        let seg_index = match self.entries_pos.binary_search(&self.pos) {
332            Ok(i) => i,
333            Err(i) => {
334                if i == 0 {
335                    0
336                } else {
337                    i - 1
338                }
339            }
340        };
341        let seg = &self.index.segments()[seg_index];
342        let start_pos = seg.data_offset();
343        let seg_pos = self.entries_pos[seg_index];
344        let skip_pos = self.pos - seg_pos;
345        let read_size = seg.saved_size();
346        match seg.flag() {
347            IndexSegmentFlag::UnCompressed => {
348                let mut lock = MutexWrapper::new(self.reader.clone(), start_pos + skip_pos);
349                let readed = (&mut lock).take(read_size - skip_pos).read(buf)?;
350                self.pos += readed as u64;
351                Ok(readed)
352            }
353            IndexSegmentFlag::Compressed => {
354                let mut cache = ZlibDecoder::new(
355                    MutexWrapper::new(self.reader.clone(), start_pos).take(read_size),
356                );
357                if skip_pos != 0 {
358                    let mut e = EmptyWriter::new();
359                    std::io::copy(&mut (&mut cache).take(skip_pos), &mut e)?; // skip
360                }
361                let readed = cache.read(buf)?;
362                self.pos += readed as u64;
363                self.cache = Some(cache);
364                Ok(readed)
365            }
366        }
367    }
368}
369
370impl<T: Read + Seek + std::fmt::Debug> Seek for Entry<T> {
371    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
372        let new_pos = match pos {
373            SeekFrom::Start(p) => p,
374            SeekFrom::End(offset) => {
375                if offset < 0 {
376                    if (-offset) as u64 > self.index.info().file_size() {
377                        return Err(std::io::Error::new(
378                            std::io::ErrorKind::InvalidInput,
379                            "Seek from end exceeds file length",
380                        ));
381                    }
382                    self.index.info().file_size() - (-offset) as u64
383                } else {
384                    self.index.info().file_size() + offset as u64
385                }
386            }
387            SeekFrom::Current(offset) => {
388                if offset < 0 {
389                    if (-offset) as u64 > self.pos {
390                        return Err(std::io::Error::new(
391                            std::io::ErrorKind::InvalidInput,
392                            "Seek from current exceeds file start",
393                        ));
394                    }
395                    self.pos - (-offset) as u64
396                } else {
397                    self.pos + offset as u64
398                }
399            }
400        };
401        if let Some(cache) = self.cache.as_mut() {
402            let old_seg_index = match self.entries_pos.binary_search(&self.pos) {
403                Ok(i) => i,
404                Err(i) => {
405                    if i == 0 {
406                        0
407                    } else {
408                        i - 1
409                    }
410                }
411            };
412            let new_seg_index = match self.entries_pos.binary_search(&new_pos) {
413                Ok(i) => i,
414                Err(i) => {
415                    if i == 0 {
416                        0
417                    } else {
418                        i - 1
419                    }
420                }
421            };
422            if old_seg_index != new_seg_index {
423                self.cache.take();
424            } else {
425                if new_pos >= self.pos {
426                    let skip_pos = new_pos - self.pos;
427                    let mut e = EmptyWriter::new();
428                    std::io::copy(&mut cache.take(skip_pos), &mut e)?; // skip
429                } else {
430                    self.cache.take();
431                }
432            }
433        }
434        self.pos = new_pos;
435        Ok(self.pos)
436    }
437
438    fn rewind(&mut self) -> std::io::Result<()> {
439        self.pos = 0;
440        self.cache.take();
441        Ok(())
442    }
443
444    fn stream_position(&mut self) -> std::io::Result<u64> {
445        Ok(self.pos)
446    }
447}
448
449struct SimpleCryptZlib<T: Read + Seek + std::fmt::Debug> {
450    inner: PrefixStream<ZlibDecoder<StreamRegion<Entry<T>>>>,
451    index: XP3FileIndex,
452}
453
454impl<T: Read + Seek + std::fmt::Debug> SimpleCryptZlib<T> {
455    fn new(mut entry: Entry<T>, index: XP3FileIndex) -> Result<Self> {
456        entry.seek(SeekFrom::Start(0x15))?;
457        let entry = StreamRegion::new(entry, 0x15, index.info().file_size())?;
458        let inner = PrefixStream::new(vec![0xFF, 0xFE], ZlibDecoder::new(entry));
459        Ok(Self { inner, index })
460    }
461}
462
463impl<T: Read + Seek + std::fmt::Debug> ArchiveContent for SimpleCryptZlib<T> {
464    fn name(&self) -> &str {
465        &self.index.info().name()
466    }
467}
468
469impl<T: Read + Seek + std::fmt::Debug> Read for SimpleCryptZlib<T> {
470    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
471        self.inner.read(buf)
472    }
473}
474
475#[derive(Debug)]
476struct SimpleCryptInner<T: Read + Seek + std::fmt::Debug> {
477    inner: StreamRegion<Entry<T>>,
478    crypt: u8,
479}
480
481impl<T: Read + Seek + std::fmt::Debug> SimpleCryptInner<T> {
482    fn new(mut entry: Entry<T>, crypt: u8) -> Result<Self> {
483        entry.seek(SeekFrom::Start(5))?;
484        let size = entry.index.info().file_size();
485        let entry = StreamRegion::new(entry, 5, size)?;
486        Ok(Self {
487            inner: entry,
488            crypt,
489        })
490    }
491}
492
493impl<T: Read + Seek + std::fmt::Debug> Read for SimpleCryptInner<T> {
494    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
495        let readed = self.inner.read(buf)?;
496        match self.crypt {
497            0 => {
498                for b in &mut buf[..readed] {
499                    let ch = *b as u16;
500                    if ch >= 20 {
501                        *b = wrapping! {ch ^ (((ch & 0xfe) << 8) ^ 1)} as u8;
502                    }
503                }
504            }
505            1 => {
506                for b in &mut buf[..readed] {
507                    let mut ch = *b as u32;
508                    ch = wrapping! {((ch & 0xaaaaaaaa) >> 1) | ((ch & 0x55555555) << 1)};
509                    *b = ch as u8;
510                }
511            }
512            _ => {}
513        }
514        Ok(readed)
515    }
516}
517
518impl<T: Read + Seek + std::fmt::Debug> Seek for SimpleCryptInner<T> {
519    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
520        self.inner.seek(pos)
521    }
522
523    fn rewind(&mut self) -> std::io::Result<()> {
524        self.inner.rewind()
525    }
526
527    fn stream_position(&mut self) -> std::io::Result<u64> {
528        self.inner.stream_position()
529    }
530}
531
532#[derive(Debug)]
533struct SimpleCrypt<T: Read + Seek + std::fmt::Debug> {
534    inner: PrefixStream<SimpleCryptInner<T>>,
535    index: XP3FileIndex,
536}
537
538impl<T: Read + Seek + std::fmt::Debug> SimpleCrypt<T> {
539    fn new(entry: Entry<T>, index: XP3FileIndex, crypt: u8) -> Result<Self> {
540        let inner = PrefixStream::new(vec![0xFF, 0xFE], SimpleCryptInner::new(entry, crypt)?);
541        Ok(Self { inner, index })
542    }
543}
544
545impl<T: Read + Seek + std::fmt::Debug> ArchiveContent for SimpleCrypt<T> {
546    fn name(&self) -> &str {
547        &self.index.info().name()
548    }
549
550    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
551        Ok(Box::new(self))
552    }
553}
554
555impl<T: Read + Seek + std::fmt::Debug> Read for SimpleCrypt<T> {
556    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
557        self.inner.read(buf)
558    }
559}
560
561impl<T: Read + Seek + std::fmt::Debug> Seek for SimpleCrypt<T> {
562    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
563        self.inner.seek(pos)
564    }
565
566    fn rewind(&mut self) -> std::io::Result<()> {
567        self.inner.rewind()
568    }
569
570    fn stream_position(&mut self) -> std::io::Result<u64> {
571        self.inner.stream_position()
572    }
573}
574
575#[derive(Debug)]
576struct MdfEntry<T: Read + Seek + std::fmt::Debug> {
577    inner: ZlibDecoder<StreamRegion<Entry<T>>>,
578    index: XP3FileIndex,
579}
580
581impl<T: Read + Seek + std::fmt::Debug> MdfEntry<T> {
582    fn new(mut entry: Entry<T>, index: XP3FileIndex) -> Result<Self> {
583        entry.seek(SeekFrom::Start(8))?;
584        let entry = StreamRegion::new(entry, 8, index.info().file_size())?;
585        let inner = ZlibDecoder::new(entry);
586        Ok(Self { inner, index })
587    }
588}
589
590impl<T: Read + Seek + std::fmt::Debug> ArchiveContent for MdfEntry<T> {
591    fn name(&self) -> &str {
592        &self.index.info().name()
593    }
594}
595
596impl<T: Read + Seek + std::fmt::Debug> Read for MdfEntry<T> {
597    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
598        self.inner.read(buf)
599    }
600}